/**
 * @overview
 * @using Company.Error.ErrorHandler
 * @using Company.Utilities.Utils
 * @using Prototype
 * @using Company
 * @using Company.Error
 * @using Company.Error.ErrorHandler
 * @using Company.Utilities
 * @using Company.UI
 * @using Company.UI.Element
 * @using TemplatesLoader.asp?variables=VerticalListjackerTpl&paths=lib/Company/UI/Templates/VerticalListjacker.htm
 */


/**
 *	@class This class creates Listjackers. 
 *	@property {Hash} listjackers Hash table of Listjackers added to the page
 */
Company.UI.ListjackerFactory = {
	listjackers: $H()
};


Company.UI.ListjackerFactory.createListjacker = function(listjackerType, listjackerID) {
	var listjacker = new Company.UI.Listjacker(listjackerType, listjackerID);
	
	this.listjackers.set(listjackerID, listjacker);
	Event.observe(window, 'resize', function(event) {
			Company.UI.ListjackerFactory.onResize(event);
		});
	return listjacker;
}


Company.UI.ListjackerFactory.findListjacker = function(listjackerID) {
	var listjacker = this.listjackers.get(listjackerID);
	return listjacker;
}


Company.UI.ListjackerFactory.onResize = function(event) {
	this.listjackers.each(function(pair) {
		pair.value.resize();
	})
}


/**
 *	@class This class loads a list of any object using AJAX, renders them on the page using templates and manages their scrolling.
 *	@property {String} listjackerID
 *	@property {Element} parent The element that contains the Listjacker
 *	@property {Element} element The div element that displays the objects
 *	@property {PeriodicalExecuter} executer A periodial executer that manages the scrolling and preloading of objects
 */
Company.UI.Listjacker = function(listjackerType, listjackerID) {
	this.listjackerID = listjackerID;
	this.listjackerType = listjackerType;
	this.ajaxUrl = "";
	this.arrayName = "";
	this.itemTemplate = "";
	this.pagesToPreBuild = 1;
	this.itemsToPrejack = 100;
	this.hSpacing = 0;
	this.vSpacing = 0;
	this.minScroll = 2;
	this.maxScroll = 64;
	this.interval = 0.02;
	this._items = $A();
	this._itemCount = null;
	this.itemsPerPage = 0;
	this.parent = null;
	this.element = null;
	this.scroller = null;
	this.executer = null;
	this.objects = $A();
	this.lastItem = 0;
	this.phrase = {
		totalResults: "%%1%% results found",
		nextResults: "next %%1%% results"
	}
};


Company.UI.Listjacker.prototype.setItemTemplate = function(itemTemplate) {
	this.itemTemplate = '<div style="float: left; position: absolute;">' + itemTemplate + '</div>';
}


Company.UI.Listjacker.prototype.setAjaxUrl = function(ajaxUrl, arrayName) {
	this.ajaxUrl = ajaxUrl;
	this.arrayName = arrayName;
	
	this.removeAllElements();
	this.insertElements();
}



Company.UI.Listjacker.prototype.insertListjacker = function(parentID) {
	this.parent = $(parentID);
	this.element = $(this.listjackerID);
	
	if (this.element == null) {
		var myTemplate = new Company.Utilities.Template(this.listjackerHtml())
		var myValues = {listjackerID: this.listjackerID};
		this.parent.innerHTML = myTemplate.evaluate(myValues);
		
		this.element = $(this.listjackerID);
	}
	
	this.scroller = this.element.down("div.ListjackerScroller");
	Event.observe(this.scroller, 'DOMMouseScroll', function(event) {
			if (event.detail > 0) {
				upOrDown = "down";
			} else {
				upOrDown = "up";
			}
			Company.UI.ListjackerFactory.findListjacker(this.up("div.ListjackerContainer").id).scroll(upOrDown);
		});
	this.resize();
}


/** 
 * Returns the HTML for the listjacker container.
 */
Company.UI.Listjacker.prototype.listjackerHtml = function() {
	return eval(this.listjackerType + "ListjackerTpl");
}


/** 
 * Preloads a number of object using AJAX.
 */
Company.UI.Listjacker.prototype.prejack = function() {
	var jsonResponse;
	
	var paramObj = {
		firstItem: this.lastItem+1,
		itemCount: this.itemsToPrejack
	}
	
	new Ajax.Request(
		this.ajaxUrl, {
			method: 'GET',
			asynchronous: false,
			parameters: paramObj,
		    onSuccess: function(transport) {
							var result = transport.responseText;
							jsonResponse = eval('(' + result + ')');
						}
		}
	);
	
	var newItems = $A(eval("jsonResponse" + this.arrayName));
	this._items = [this._items, newItems].flatten();
	
	this.lastItem += newItems.length;
	this._itemCount = this._items.length;
	
	this.setPageUpText(this._itemCount);
}


/** 
 *	Returns the item with the given index, calling prejack if necessary.
 *	@param {Number} itemIndex The ordinal index of the item requested
 */
Company.UI.Listjacker.prototype.getItem = function(itemIndex) {
	return(this._items[itemIndex]);
}


/** 
 *	Returns the total number of items, prejacked or not.
 *	@param {Number} itemIndex The ordinal index of the item requested
 */
Company.UI.Listjacker.prototype.getItemCount = function(itemIndex) {
	if (this._itemCount == null) {
		this.prejack();
	}
	return(this._itemCount);
}


Company.UI.Listjacker.prototype.insertElements = function() {
	this.insertBelow();
	this.insertAbove();
	this.removeInvisibleElements();
}


/**
 *	Inserts elements starting at the top of the current page, and going down
 */
Company.UI.Listjacker.prototype.insertBelow = function(nextItemIndex) {
	var pageWidth = this.scroller.getWidth();
	var pageHeight = this.scroller.getHeight();

	var maxTop = pageHeight * (this.pagesToPreBuild+1); //controls how many elements to insert
	var lastRowHeight = 0; //height of the previous row of elements
	var posLeft = 0;	//left position of the next element
	var posTop;		//top position of the next element
	
	if (this.scroller.childElements().length == 0) {
		posTop = 0;
		this.itemsPerPage = 0;
	} else {
		posTop = this.getPosTop();
	}
	if (nextItemIndex == null) {
		if (this.scroller.childElements().length == 0) {
			nextItemIndex = 0;
		} else {
			nextItemIndex = this.scroller.lastChild.itemIndex+1;
		}
	}


	while (nextItemIndex < this.getItemCount()) {
		var item = this.getItem(nextItemIndex);
		
		var myTemplate = new Company.Utilities.Template(this.itemTemplate);
		var html = myTemplate.evaluate(item);
		new Insertion.Bottom(this.scroller, html);
		var div = $(this.scroller.lastChild);
		div.itemIndex = nextItemIndex;
		var width = div.getWidth();
		var height = div.getHeight();
		
		if (posLeft == 0) {
			//first item in a row
			div.moveTo(0, posTop);
			lastRowHeight = height;
		} else {
			//subsequent items in a row
			if (posLeft + width > pageWidth) {
				//wrap to next row
				posLeft = 0;
				posTop += this.vSpacing + lastRowHeight;
				if (posTop > maxTop) {
					div.remove();
					break;
				}
			}
			div.moveTo(posLeft, posTop);
		}
		posLeft += this.hSpacing + width;
		
		//remember the height of the tallest element in this row
		if (height > lastRowHeight) lastRowHeight = height;
		
		if (posTop < pageHeight) {
			this.itemsPerPage++;
		}
		
		nextItemIndex++;
	};
	
	this.setPageDownText(this.itemsPerPage);
}


/**
 *	Inserts elements starting at the top of the current page, and going up
 */
Company.UI.Listjacker.prototype.insertAbove = function() {
	if (this.scroller.childElements().length == 0 || this.scroller.firstChild.itemIndex == 0) {
		return;
	}
	
	var pageWidth = this.scroller.getWidth();
	var pageHeight = this.scroller.getHeight();

	var minBottom = pageHeight * this.pagesToPreBuild * -1; //controls how many elements to insert
	var thisRowHeight = 0; //height of the current row of elements
	var posLeft = 0;	//left position of the next element
	var posBottom = this.scroller.firstChild.offsetTop;		//bottom of the next element
	var nextItemIndex = this.scroller.firstChild.itemIndex-1;
	
	while (nextItemIndex >= 0) {
		var item = this.getItem(nextItemIndex);
		
		var myTemplate = new Company.Utilities.Template(this.itemTemplate);
		var html = myTemplate.evaluate(item);
		new Insertion.Top(this.scroller, html);
		var div = $(this.scroller.firstChild);
		div.itemIndex = nextItemIndex;
		var width = div.getWidth();
		var height = div.getHeight();
		
		if (posLeft == 0) {
			//first item in a row
			div.moveTo(0, posBottom-this.vSpacing-height);
			thisRowHeight = height;
		} else {
			//subsequent items in a row
			if (posLeft + width > pageWidth) {
				//**** TODO: correct position of elements in this row
				//wrap to next row
				posLeft = 0;
				posBottom -= this.vSpacing + thisRowHeight;
				if (posBottom < minBottom) {
					div.remove();
					break;
				}
			}
			div.moveTo(posLeft, posBottom-this.vSpacing-height);
		}
		posLeft += this.hSpacing + width;
		
		//remember the height of the tallest element in this row
		if (height > thisRowHeight) thisRowHeight = height;
		
		nextItemIndex--;
	}
}


/**
 *	Removes any elements that are outside of the range of pagesToPreBuild (always called by insertElements())
 */
Company.UI.Listjacker.prototype.removeInvisibleElements = function() {
	pageHeight = this.scroller.getHeight();

	var maxTop = pageHeight * (this.pagesToPreBuild+1);
	var minBottom = pageHeight * this.pagesToPreBuild * -1;
	
	// build an array of elements that are beyond the range of pagesToPreBuild
	var elementsToRemove = $A();
	var elements = this.scroller.childElements();
	for (var i = 0; i < elements.length; i++) {
		var elem = $(elements[i]);
		if (elem.offsetTop > maxTop || (elem.offsetTop+elem.getHeight()) < minBottom) {
			elementsToRemove.push(elem);
		}
	}
	
	// now remove them
	elementsToRemove.each(	function(elem) {
								elem.remove();
							});
}


/**
 *	Returns the top position of the next row to be inserted
 */
Company.UI.Listjacker.prototype.getPosTop = function() {
	var elements = this.scroller.childElements();
	var i = elements.size()-1;
	
	//find the highest posTop by looking at each element from the last element backwards
	var posTop = 0;
	var rowTop = 0;
	while (i >= 0) {
		var elem = elements[i];
		var elemTop = elem.offsetTop;
		var elemBottom = elem.offsetTop + elem.getHeight() + this.vSpacing;
		
		if (elemTop < rowTop) {
			break;
		} else {
			rowTop = elemTop;
		}
		
		if (elemBottom > posTop) {
			posTop = elemBottom;
		}
		i--;
	}
	
	return posTop;
}


/**
 *	Returns the item index of the element in the top left corner
 */
Company.UI.Listjacker.prototype.getFirstVisibleItemIndex = function() {
	var elements = this.scroller.childElements();
	for (var i = 0; i < elements.length; i++) {
		var elem = $(elements[i]);
		if (elem.offsetTop >= 0) {
			//this is the first visible element
			return elem.itemIndex;
		}
	}
}


/**
 *	Scrolls up or down by a full page
 *	@param {String} upOrDown "up" or "down"
 */
Company.UI.Listjacker.prototype.scroll = function(upOrDown) {
	//if an executer is running, stop it first
	if (this.executer != null) {
		this.executer.stop();
		this.executer = null;
		this.insertElements();
	}

	this.executer = new PeriodicalExecuter(function(pe){
		pe.listjacker.scrollStep()
	}, this.interval);
	this.executer.listjacker = this;
	
	if (upOrDown == "up") {
		this.executer.byPixels = this.minScroll;
	} else {
		this.executer.byPixels = this.minScroll*-1;
	}
	
	this.executer.amount = 0;
	var elements = this.scroller.childElements();
	for (var i = 0; i < elements.length; i++) {
		var elem = $(elements[i]);
		if (upOrDown == "up") {
			if (Math.abs(elem.offsetTop) < pageHeight) {
				this.executer.amount = Math.abs(elem.offsetTop);
				break;
			}
		} else {
			if (elem.offsetTop + elem.getHeight() > pageHeight) {
				//this is the lowest visible element
				this.executer.amount = elem.offsetTop;
				break;
			}
		}
	}

}


Company.UI.Listjacker.prototype.scrollStep = function() {
	if (this.executer.amount <= 0) {
		this.executer.stop();
		this.executer = null;
		this.insertElements();
		return;
	}
	
	var byPixels = this.executer.byPixels;
	if (this.executer.amount-Math.abs(byPixels) < 0) {
		if (byPixels < 0) {
			byPixels = this.executer.amount * -1;
		} else {
			byPixels = this.executer.amount;
		}
	}
	
	var elements = this.scroller.childElements();
	for (var i = 0; i < elements.length; i++) {
		var elem = $(elements[i]);
		elem.moveBy(0, byPixels);
	}
	
	this.executer.amount -= Math.abs(byPixels);

	//accelerate scrolling
	if (Math.abs(this.executer.byPixels)*2 <= this.maxScroll) {
		this.executer.byPixels = this.executer.byPixels * 2;
	}
	
	//decelerate scrolling
	if (this.executer.amount <= this.maxScroll) {
		this.executer.byPixels = Math.round(this.executer.byPixels / 2);
	}
}


Company.UI.Listjacker.prototype.resize = function(newWidth, newHeight) {
	var oldWidth = this.element.getWidth();
	var oldHeight = this.element.getHeight();
	
	if (newWidth == null) newWidth = this.parent.getWidth();
	if (newHeight == null) newHeight = this.parent.getHeight();
	
	var pageUp = this.element.down("div.ListjackerPageUp");
	var pageDown = this.element.down("div.ListjackerPageDown");
	
	var pageUpHeight = pageUp.getHeight();
	var pageDownHeight = pageDown.getHeight();
	
	this.element.sizeTo(newWidth, newHeight);
	
	pageUp.sizeTo(newWidth, null);
	pageDown.sizeTo(newWidth, null);
	pageDown.moveTo(0, newHeight-pageDownHeight);
	
	this.scroller.moveTo(0, pageUpHeight);
	this.scroller.sizeTo(newWidth, newHeight-pageUpHeight-pageDownHeight);
	
	if (newWidth != oldWidth) {
		this.removeAllElements();
		this.insertElements(this.getFirstVisibleItemIndex());
	}
}


Company.UI.Listjacker.prototype.removeAllElements = function(event) {
	this.scroller.setToEmpty();
}


Company.UI.Listjacker.handlePageClick = function(element, upOrDown) {
	element = $(element);
	var listjackerID = element.up("div.ListjackerContainer").id;
	Company.UI.ListjackerFactory.findListjacker(listjackerID).scroll(upOrDown);
}


Company.UI.Listjacker.prototype.setPageUpText = function(itemCount) {
	var pageUpTextElement = this.element.down(".ListjackerPageUpText");
	var pageUpText = this.phrase.totalResults.replace(/\%\%1\%\%/, itemCount);
	pageUpTextElement.innerHTML = pageUpText;
}


Company.UI.Listjacker.prototype.setPageDownText = function(itemsPerPage) {
	var pageDownTextElement = this.element.down(".ListjackerPageDownText");
	var pageDownText = this.phrase.nextResults.replace(/\%\%1\%\%/, itemsPerPage);
	pageDownTextElement.innerHTML = pageDownText;
}